/*
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

using Gtk;
using Glade;
using Mono.Unix;

using DCSharp.Backend.Connections;
using DCSharp.Backend.Objects;
using DCSharp.Backend.Managers;

namespace DCSharp.Gui
{
	public class SearchPage : Page
	{
		private SearchManager searchManager;

		private SearchInfo searchInfo;
		private MessageArea messageArea;
		private SizeGroup sizeGroup;
		private Widget focusWidget;
		private bool searchEnabled;

		private SearchResultStore searchStore;
		private TreeModelFilter filterModel;
		private TreeModelSort sortModel;
		private Dictionary<string, SearchResult> alwaysVisibleResults;

		private SortType sortOrder;
		private int sortColumnId;

		private ArrayList addQueue;
		private int count;

		private string tooltip;
		private ActionGroup actionGroup;

		[Widget]
		private Box searchBox;

		[Widget]
		private Box criteriaBox;

		[Widget]
		private Button showCriteriaButton;

		[Widget]
		private Button hideCriteriaButton;

		[Widget]
		private Label searchLabel;

		[Widget]
		private HistoryEntry searchEntry;

		[Widget]
		private ComboBox sizeRestrictComboBox;

		[Widget]
		private SpinButton sizeSpinButton;

		[Widget]
		private ComboBox sizeComboBox;

		[Widget]
		private Label fileTypeLabel;

		[Widget]
		private SearchFileTypeComboBox fileTypeComboBox;

		[Widget]
		private SearchResultView searchResultView;

		[Widget]
		private CheckButton freeSlotsCheckButton;

		[Widget]
		private Button searchButton;

		#region Constructors

		public SearchPage(SearchManager searchManager) : base("SearchPage.glade")
		{
			if (searchManager == null)
			{
				throw new ArgumentNullException("searchManager");
			}
			this.searchManager = searchManager;

			addQueue = ArrayList.Synchronized(new ArrayList());
			alwaysVisibleResults = new Dictionary<string, SearchResult>();

			// User interface
			ActionEntry[] entries = new ActionEntry[] {
				new ActionEntry("VisibleColumns", null, Catalog.GetString("_Visible Columns..."), null,
					Catalog.GetString("Edit visible columns"),
					null),
				new ActionEntry("SearchTTH", Stock.Find, Catalog.GetString("_Search For This File"), null,
					Catalog.GetString("Does a search for the selected file"),
					OnSearchTTH)
			};

			actionGroup = new ActionGroup("SearchPageActions");
			actionGroup.Add(entries);

			// Model
			searchStore = new SearchResultStore();

			filterModel = new TreeModelFilter(searchStore, null);
			filterModel.RowInserted += OnRowInserted;
			filterModel.RowDeleted += OnRowDeleted;
			filterModel.VisibleFunc = FilterSearchResults;

			sortModel = new TreeModelSort(filterModel);
			sortModel.SetSortColumnId((int)SearchResultStore.Column.Name,
				SortType.Ascending);

			// View
			searchResultView.Model = sortModel;
			searchResultView.PopulatePopup += OnPopulatePopup;
			searchResultView.Show();

			searchResultView.GetColumn("TTH").Visible = false;

			// Set the text style for the columns.
			foreach (TreeViewColumn column in searchResultView.Columns)
			{
				foreach (CellRenderer renderer in column.CellRenderers)
				{
					if (renderer is CellRendererText)
					{
						column.SetCellDataFunc(renderer, SetTextStyle);
					}
				}
			}

			// Visible columns
			VisibleColumnsWindow.ConnectToWidget(this, actionGroup["VisibleColumns"],
				searchResultView.Columns, searchResultView.Columns[0]);

			// Widgets
			sizeGroup = new SizeGroup(SizeGroupMode.Horizontal);
			sizeGroup.AddWidget(searchLabel);
			sizeGroup.AddWidget(fileTypeLabel);

			showCriteriaButton.Name = hideCriteriaButton.Name = "dcsharp-criteria-button";
			showCriteriaButton.Clicked += OnToggleCriteria;
			hideCriteriaButton.Clicked += OnToggleCriteria;

			searchEntry.Activated += OnSearchActivated;
			searchEntry.Changed += delegate
			{
				searchButton.Sensitive = searchEntry.Text.Length > 0 &&
					searchEnabled;
			};
			searchEntry.Show();
			searchEntry.GrabFocus();

			sizeRestrictComboBox.Active = 0;
			sizeComboBox.Active = 2;

			sizeSpinButton.Activated += OnSearchActivated;
			searchButton.Clicked += OnSearchActivated;
			freeSlotsCheckButton.Toggled += OnFilterToggled;

			fileTypeComboBox.Show();

			searchEnabled = true;
			UpdateSensitivity();
		}

		#endregion

		#region Properties

		public override string Title
		{
			get { return Catalog.GetString("Search"); }
		}

		public override Gdk.Pixbuf Icon
		{
			get { return RenderIcon(Stock.Find, IconSize.Menu, null); }
		}

		public override string Tooltip
		{
			get
			{
				if (tooltip == null)
				{
					return Catalog.GetString("Search for files");
				}
				return tooltip;
			}
		}

		public override ActionGroup Actions
		{
			get { return actionGroup; }
		}

		public int VisibleColumns
		{
			get { return Util.GetVisibleColumns(searchResultView.Columns); }
			set { Util.SetVisibleColumns(searchResultView.Columns, value | 1); }
		}

		#endregion

		#region Methods

		public void Search(SearchInfo searchInfo)
		{
			if (searchInfo == null)
			{
				throw new ArgumentNullException("searchInfo");
			}
			// Update the gui to reflect the search criteria
			if (searchInfo.TTH != null)
			{
				searchEntry.Text = "TTH:" + searchInfo.TTH;
			}
			else
			{
				throw new NotImplementedException();
			}
			PerformSearch(searchInfo);
		}

		public void StopSearch()
		{
			if (searchButton.Label != Stock.Find)
			{
				addQueue.Clear();

				tooltip = null;
				IsWorking = false;

				searchButton.Label = Stock.Find;
				searchBox.Sensitive = criteriaBox.Sensitive = true;

				Window window = Toplevel as Window;
				if (window != null && (window.Focus == null ||
					window.Focus == searchButton))
				{
					focusWidget.GrabFocus();
				}

				UpdateStatus();
			}
			if (searchInfo != null)
			{
				searchManager.StopSearch(searchInfo);
				searchManager.ResultReceived -= OnResultReceived;
				searchManager.SearchStopped -= OnSearchStopped;
				searchInfo = null;
			}
		}

		public void FocusSearchEntry()
		{
			searchEntry.GrabFocus();
		}

		public void SetSearchEnabled(bool enabled)//, string message)
		{
			searchEnabled = enabled;
			UpdateSensitivity();
		}

		protected override void OnDestroyed()
		{
			StopSearch();
			base.OnDestroyed();
		}

		protected override bool OnKeyPressEvent(Gdk.EventKey evnt)
		{
			if (evnt.Key == Gdk.Key.Escape)
			{
				StopSearch();
				return true;
			}
			return base.OnKeyPressEvent(evnt);
		}

		private void PerformSearch(SearchInfo searchInfo)
		{
			StopSearch();

			if (!searchEnabled)
			{
				return;
			}
			this.searchInfo = searchInfo;

			Status = String.Format(Catalog.GetString("Searching for {0}..."),
				searchEntry.Text);
			tooltip = Status;

			Window window = Toplevel as Window;
			if (window != null)
			{
				focusWidget = window.Focus;
			}

			IsWorking = true;

			searchBox.Sensitive = criteriaBox.Sensitive = false;
			searchButton.Label = Stock.Stop;
			searchButton.GrabFocus();

			searchStore.Clear();
			alwaysVisibleResults.Clear();

			searchManager.ResultReceived += OnResultReceived;
			searchManager.SearchStopped += OnSearchStopped;
			searchManager.Search(searchInfo, new TimeSpan(0, 0, 10));

			OnPageChanged();
		}

		private bool FilterSearchResults(TreeModel model, TreeIter iter)
		{
			if (!freeSlotsCheckButton.Active)
			{
				return true;
			}

			SearchResult result = model.GetValue(iter,
				(int)SearchResultStore.Column.Object) as SearchResult;

			if (result != null && (result.FreeSlots > 0 ||
				alwaysVisibleResults.ContainsValue(result)))
			{
				return true;
			}
			return false;
		}

		private void UpdateSensitivity()
		{
			if (!searchEnabled)
			{
				if (messageArea == null)
				{
					messageArea = new MessageArea(Catalog.GetString("Before you can search you have to connect to one or more hubs"),
						null, MessageType.Info);
					Add(messageArea);
				}

				messageArea.Show();
			}
			else if (messageArea != null)
			{
				messageArea.Hide();
			}

			searchButton.Sensitive = searchEntry.Text.Length > 0 &&
				searchEnabled;
		}

		private void UpdateStatus()
		{
			if (count > 0)
			{
				Status = String.Format(Catalog.GetPluralString("{0} file found",
					"{0} files found", count), count);
			}
			else
			{
				Status = Catalog.GetString("No files found");
			}
		}

		private SearchInfo CreateSearchInfo()
		{
			// Keywords
			string pattern = searchEntry.Text.Trim();
			if (pattern.ToUpper().StartsWith("TTH:"))
			{
				return new SearchInfo(pattern.Substring(4));
			}

			string[] keywords = Regex.Split(pattern, @"\s+");

			// Type
			SearchFileType type = fileTypeComboBox.ActiveType;

			// Size
			long? size = (long)sizeSpinButton.Value;
			if (size <= 0)
			{
				size = null;
			}
			else if (sizeComboBox.Active > 0)
			{
				size *= (long)Math.Pow(1024, sizeComboBox.Active);
			}

			bool isMaximumSize = sizeRestrictComboBox.Active == 1;
			long? maxSize = isMaximumSize ? size : null;
			long? minSize = !isMaximumSize ? size : null;

			return new SearchInfo(keywords, type, maxSize, minSize);
		}

		private void OnToggleCriteria(object obj, EventArgs args)
		{
			bool showClicked = obj == showCriteriaButton;

			criteriaBox.Visible = showClicked;
			showCriteriaButton.Sensitive = !showClicked;
			sizeGroup.Mode = showClicked ? SizeGroupMode.Horizontal : SizeGroupMode.None;

			if (!showClicked)
			{
				fileTypeComboBox.Active = 0;
				sizeSpinButton.Value = 0;
			}
		}

		private void OnSearchActivated(object obj, EventArgs args)
		{
			if (searchEntry.Text.Length == 0 || !searchEnabled)
			{
				return;
			}

			if (searchButton.Label == Stock.Find)
			{
				PerformSearch(CreateSearchInfo());
			}
			else
			{
				StopSearch();
			}
		}

		private void OnPopulatePopup(object obj, PopulatePopupArgs args)
		{
			Menu menu = args.Menu;

			MenuItem item = new SeparatorMenuItem();
			menu.Append(item);
			item.Show();

			// Search by TTH
			Action searchAction = actionGroup["SearchTTH"];
			searchAction.Sensitive = searchButton.Sensitive && GetSelectedTTH() != null;
			menu.Append(searchAction.CreateMenuItem());
		}

		private void OnSearchTTH(object obj, EventArgs args)
		{
			string tth = GetSelectedTTH();
			if (tth != null && searchButton.Sensitive)
			{
				Search(new SearchInfo(tth));
			}
		}

		private string GetSelectedTTH()
		{
			object[] results = searchResultView.GetSelectedObjects(
				(int)SearchResultStore.Column.Object);

			if (results.Length == 0)
			{
				return null;
			}

			string tth = ((SearchResult)results[0]).TTH;
			foreach (SearchResult result in results)
			{
				if (result.TTH != tth)
				{
					return null;
				}
			}
			return tth;
		}

		private void OnFilterToggled(object obj, EventArgs args)
		{
			filterModel.Refilter();
		}

		private void OnRowInserted(object obj, RowInsertedArgs args)
		{
			// Do not count child rows
			if (args.Path.Indices.Length == 1)
			{
				count++;
				UpdateStatus();
			}
		}

		private void OnRowDeleted(object obj, RowDeletedArgs args)
		{
			// Do not count child rows 
			if (args.Path.Indices.Length == 1)
			{
				count--;
				UpdateStatus();
			}
		}

		private void SetTextStyle(TreeViewColumn column, CellRenderer renderer,
			TreeModel model, TreeIter iter)
		{
			CellRendererText textRenderer = renderer as CellRendererText;
			if (textRenderer != null)
			{
				if (freeSlotsCheckButton.Active)
				{
					SearchResult result = (SearchResult)model.GetValue(iter,
						(int)SearchResultStore.Column.Object);

					if (result.FreeSlots == 0 &&
						alwaysVisibleResults.ContainsValue(result))
					{
						textRenderer.Foreground = "dim grey";
						return;
					}
				}
				textRenderer.Foreground = "black";
			}
		}

		private void OnResultReceived(object obj, ResultReceivedEventArgs args)
		{
			if (args.SearchInfo != searchInfo)
			{
				return;
			}
			bool addIdleHandler = addQueue.Count == 0;
			addQueue.Add(args.Result);

			if (addIdleHandler)
			{
				GLib.Idle.Add(AddResultsInQueue);
			}
		}

		private void OnSearchStopped(object obj, SearchEventArgs args)
		{
			if (args.SearchInfo == searchInfo)
			{
				Application.Invoke(delegate
				{
					StopSearch();
				});
			}
		}

		private bool AddResultsInQueue()
		{
			long ticks = DateTime.Now.Ticks;

			DisableSorting();
			while (addQueue.Count > 0)
			{
				SearchResult result = (SearchResult)addQueue[0];
				addQueue.Remove(result);

				TreeIter iter = searchStore.AddResult(result);

				// Handle the case where the parent should be invisible (no
				// slots), but there are children that should be visible.
				if (result.TTH != null && result.FreeSlots != 0 &&
					!alwaysVisibleResults.ContainsKey(result.TTH))
				{
					TreeIter parent;
					if (searchStore.IterParent(out parent, iter))
					{
						SearchResult parentResult = (SearchResult)searchStore.GetValue(
							parent, (int)SearchResultStore.Column.Object);

						if (parentResult.FreeSlots == 0)
						{
							alwaysVisibleResults.Add(result.TTH, parentResult);
						}

						// Refilter the parent row to make the expander arrow
						// visible. This is to work around a bug in TreeModelFilter.
						if (searchStore.IterNChildren(parent) == 1)
						{
							TreePath parentPath = searchStore.GetPath(parent);
							searchStore.EmitRowChanged(parentPath,
								parent);
						}
					}
				}

				// Are we blocking the GUI?
				if ((DateTime.Now.Ticks - ticks) >= 400000)
				{
					RestoreSorting();
					return true;
				}
			}
			RestoreSorting();
			OnPageChanged();
			return false;
		}

		private void DisableSorting()
		{
			if (sortModel.GetSortColumnId(out sortColumnId,
				out sortOrder))
			{
				sortModel.SetSortColumnId(-1, SortType.Ascending);
			}
		}

		private void RestoreSorting()
		{
			sortModel.SetSortColumnId(sortColumnId, sortOrder);
		}

		#endregion
	}
}
